---
title: 网络请求的去抖动或取消
---

import { Link } from "@site/src/components/Link";
import { AutoSnippet, When } from "@site/src/components/CodeSnippet";
import homeScreen from "!raw-loader!./cancel/home_screen.dart";
import extension from "!raw-loader!./cancel/extension.dart";
import detailScreen from "/docs/case_studies/cancel/detail_screen";
import detailScreenCancel from "/docs/case_studies/cancel/detail_screen_cancel";
import detailScreenDebounce from "/docs/case_studies/cancel/detail_screen_debounce";
import providerWithExtension from "/docs/case_studies/cancel/provider_with_extension";

<!---
As applications grow in complexity, it's common to have multiple network requests
in flight at the same time. For example, a user might be typing in a search box
and triggering a new request for each keystroke. If the user types quickly, the
application might have many requests in flight at the same time.
-->
随着应用程序变得越来越复杂，同时处理多个网络请求是很常见的。
例如，用户可能在搜索框中键入内容并为每次击键触发新的请求。
如果用户打字速度很快，应用程序可能会同时处理许多请求。

<!---
Alternatively, a user might trigger a request, then navigate to a different page
before the request completes. In this case, the application might have a request
in flight that is no longer needed.
-->
或者，用户可能会触发请求，然后在请求完成之前导航到不同的页面。
在这种情况下，应用程序可能有一个不再需要的正在运行的请求。

<!---
To optimize performance in those situations, there are a few techniques you can
use:
-->
要在这些情况下优化性能，您可以使用以下几种技术：

<!---
- "Debouncing" requests. This means that you wait until the user has stopped
  typing for a certain amount of time before sending the request. This ensures
  that you only send one request for a given input, even if the user types
  quickly.
- "Cancelling" requests. This means that you cancel a request if the user
  navigates away from the page before the request completes. This ensures that
  you don't waste time processing a response that the user will never see.
-->
- “去抖动”请求。这意味着您要等到用户停止输入一段时间后再发送请求。
  这可确保即使用户键入速度很快，您也只会针对给定输入发送一个请求。
- “取消”请求。这意味着如果用户在请求完成之前离开页面，您将取消请求。
  这可确保您不会浪费时间处理用户永远不会看到的响应。

<!---
In Riverpod, both of these techniques are can be implemented in a similar way.
The key is to use `ref.onDispose` combined with "automatic disposal" or `ref.watch`
to achieve the desired behavior.
-->
在 Riverpod 中，这两种技术都可以以类似的方式实现。
关键是使用 `ref.onDispose` 方法与“自动处置”或 `ref.watch`
结合来实现所需的行为。

<!---
To showcase this, we will make a simple application with two pages:
-->
为了展示这一点，我们将制作一个包含两个页面的简单应用程序：

<!---
- A home screen, with a button which opens a new page
- A detail page, which displays a random activity from the [Bored API](https://www.boredapi.com/),
  with the ability to refresh the activity.  
  See <Link documentID="case_studies/pull_to_refresh" /> for information
  on how to implement pull-to-refresh.
-->
- 主屏幕，带有打开新页面的按钮
- 详细信息页面，显示来自 [Bored API](https://www.boredapi.com/)
  的随机活动，并且能够刷新活动。
  有关如何实现下拉刷新的信息，
  请参阅<Link documentID="case_studies/pull_to_refresh" />。

<!---
We will then implement the following behaviors:
-->
然后我们将实现以下行为：

<!---
- If the user opens the detail page and then navigates back immediately,
  we will cancel the request for the activity.
- If the user refreshes the activity multiple times in a row, we will debounce
  the requests so that we only send one request after the user stops refreshing.
-->
- 如果用户打开详细信息页面然后立即导航回来，
  我们将取消该活动的请求。
- 如果用户连续多次刷新活动，我们将对请求进行去抖动，
  以便在用户停止刷新后仅发送一个请求。

<!---
## The application
-->
## 应用​

<!---
<img
  src="/img/case_studies/cancel/app.gif"
  alt="Gif showcasing the application, opening the detail page and refreshing the activity."
/>
-->
<img
  src="/img/case_studies/cancel/app.gif"
  alt="展示应用程序、打开详细页面和刷新活动的 Gif。"
/>

<!---
First, let's create the application, without any debouncing or cancelling.  
We won't use anything fancy here, and stick to a plain `FloatingActionButton` with
a `Navigator.push` to open the detail page.
-->
首先，让我们创建应用程序，不进行任何去抖动或取消操作。  
我们不会在这里使用任何花哨的东西，而是坚持使用普通的 `FloatingActionButton`
和 `Navigator.push` 来打开详细信息页面。

<!---
First, let's start with defining our home screen. As usual,
let's not forget to specify a `ProviderScope` at the root of our application.
-->
首先，让我们从定义主屏幕开始。像往常一样，
我们不要忘记在应用程序的根组件上指定 `ProviderScope` 。

<AutoSnippet title="lib/src/main.dart" raw={homeScreen} />

<!---
Then, let's define our detail page.
To fetch the activity and implement pull-to-refresh, refer
to the <Link documentID="case_studies/pull_to_refresh" /> case study.
-->
然后，让我们定义我们的详细信息页面。
要获取活动并实施下拉刷新，
请参阅<Link documentID="case_studies/pull_to_refresh" />应用案例。

<AutoSnippet title="lib/src/detail_screen.dart" {...detailScreen} />

<!---
## Cancelling requests
-->
## 取消请求

<!---
Now that we have a working application, let's implement the cancellation logic.
-->
现在我们有了一个可以运行的应用程序，让我们实现取消逻辑。

<!---
To do so, we will use `ref.onDispose` to cancel the request when the user
navigates away from the page. For this to work, it is important that the
automatic disposal of providers is enabled.
-->
为此，我们将在用户离开页面时使用 `ref.onDispose` 取消请求。
为了使其运作，启用提供者程序的自动处置非常重要。

<!---
The exact code needed to cancel the request will depend on the HTTP client.
In this example, we will use `package:http`, but the same principle applies
to other clients.
-->
取消请求所需的确切代码取决于 HTTP 客户端。
在此示例中，我们将使用 `package:http` ，
但相同的原则也适用于其他客户端。

<!---
The key here that `ref.onDispose` will be called when the user navigates away.
That is because our provider is no-longer used, and therefore disposed
thanks to automatic disposal.  
We can therefore use this callback to cancel the request. When using `package:http`,
this can be done by closing our HTTP client.
-->
这里的关键点是当用户离开时将调用 `ref.onDispose`。
这是因为我们的提供者程序不再使用，因此通过自动处置进行了处置。  
因此，我们可以使用此回调来取消请求。
当使用 `package:http` 时，可以通过关闭 HTTP 客户端来完成。

<AutoSnippet {...detailScreenCancel} />

<!---
## Debouncing requests
-->
## 请求​去抖

<!---
Now that we have implemented cancellation, let's implement debouncing.  
At the moment, if the user refreshes the activity multiple times in a row,
we will send a request for each refresh.
-->
现在我们已经实现了取消，让我们实现去抖动。  
目前，如果用户连续多次刷新活动，
我们将为每次刷新发送一个请求。

<!---
Technically speaking, now that we have implemented cancellation, this is not
a problem. If the user refreshes the activity multiple times in a row,
the previous request will be cancelled, when a new request is made.
-->
从技术上来说，既然我们已经实行了取消，这就不成问题了。
如果用户连续多次刷新活动，
则当发出新请求时，先前的请求将被取消。

<!---
However, this is not ideal. We are still sending multiple requests, and
wasting bandwidth and server resources.  
What we could instead do is delay our requests until the user stops refreshing
the activity for a fixed amount of time.
-->
然而，这并不理想。我们仍然发送多个请求，
浪费带宽和服务器资源。  
相反，我们可以做的是延迟我们的请求，
直到用户在固定的时间内停止刷新活动。

<!---
The logic here is very similar to the cancellation logic. We will again
use `ref.onDispose`. However, the idea here is that instead of
closing an HTTP client, we will rely on `onDispose` to abort the request
before it starts.  
We will then arbitrarily wait for 500ms before sending the request.
Then, if the user refreshes the activity again before the 500ms have elapsed,
`onDispose` will be invoked, aborting the request.
-->
这里的逻辑和取消逻辑非常相似。
我们将再次使用 `ref.onDispose`。然而，这里的想法是，
我们将依靠 `onDispose` 在请求开始之前中止请求，
而不是关闭 HTTP 客户端。  
然后我们会任意等待 500ms，然后再发送请求。
然后，如果用户在 500 毫秒过去之前再次刷新活动，
将调用 `onDispose` 并中止请求。

:::info
<!---
To abort requests, a common practice is to voluntarily throw.  
It is safe to throw inside providers after the provider has been disposed.
The exception will naturally be caught by Riverpod and be ignored.
-->
要中止请求，常见的做法是主动抛出。  
在提供者程序被处置后，将提供者程序内部抛出异常是安全的。
该异常自然会被 Riverpod 捕获并被忽略。
:::

<AutoSnippet {...detailScreenDebounce} />

<!---
## Going further: Doing both at once
-->
## 更进一步：同时做这两件事​

<!---
We now know how to debounce and cancel requests.  
But currently, if we want to do another request, we need to copy-paste
the same logic in multiple places. This is not ideal.
-->
我们现在知道如何取消和取消请求。  
但目前，如果我们想做另一个请求，
我们需要将相同的逻辑复制粘贴到多个位置。这并不理想。

<!---
However, we can go further and implement a reusable utility to do both at once.
-->
然而，我们可以更进一步，实现一个可重用的实用程序来同时完成这两个任务。

<!---
The idea here is to implement an extension method on `Ref` that will
handle both cancellation and debouncing in a single method.
-->
这里的想法是在 `Ref` 上实现一个扩展方法，
该方法将在单个方法中处理取消和去抖。

<AutoSnippet raw={extension} />

<!---
We can then use this extension method in our providers as followed:
-->
然后我们可以在我们的提供者程序中使用此扩展方法，如下所示：

<AutoSnippet {...providerWithExtension} />
